// $Id: response.c 87 2011-05-04 17:34:28Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2010 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <string.h>

#include <nio.h>
#include <nio/buffer.h>

#include "log.h"
#include "headers.h"

#include "response.h"

struct niohttp_response *niohttp_request_create_response(
		struct niohttp_request *req, int code, const char *reason)
{
	int rc;
	struct niohttp_response *resp;

	if (!(resp = niohttp_response_new())) {
		niohttp_error("failed to create response");
		goto error;
	}

	if ((rc = niohttp_response_set_protocol(resp,
			niohttp_request_protocol(req)))) {
		niohttp_error("failed to set response protocol: %s",
				niohttp_strerror(rc));
		goto error;
	}

	if ((rc = niohttp_response_set_version(resp,
			niohttp_request_version(req)))) {
		niohttp_error("failed to set response version: %s",
				niohttp_strerror(rc));
		goto error;
	}

	if ((rc = niohttp_response_set_status(resp, code, reason))) {
		niohttp_error("failed to set response status: %s",
				niohttp_strerror(rc));
		goto error;
	}

	return resp;

error:
	niohttp_response_free(resp);
	return NULL;
}

struct niohttp_response *niohttp_response_new(void)
{
	struct niohttp_response *resp;

	niohttp_debug("constructing");

	if (!(resp = calloc(1, sizeof(struct niohttp_response)))) {
		niohttp_error("failed to calloc");
		goto error;
	}

	if (!(resp->header = niohttp_headers_new())) {
		niohttp_error("failed to create header");
		goto error;
	}

	return resp;

error:
	niohttp_response_free(resp);
	return NULL;
}

void niohttp_response_free(struct niohttp_response *resp)
{
	if (!resp) {
		return;
	}

	niohttp_debug("destructing");

	if (resp->reason) {
		free(resp->reason);
	}

	if (resp->version.string) {
		free(resp->version.string);
	}

	if (resp->protocol) {
		free(resp->protocol);
	}

	if (resp->header) {
		niohttp_headers_free(resp->header);
	}

	free(resp);
}

inline const char *niohttp_response_protocol(struct niohttp_response *resp)
{
	return resp->protocol;
}

inline int niohttp_response_set_protocol(struct niohttp_response *resp,
		const char *protocol)
{
	return niohttp_response_nset_protocol(resp, protocol, strlen(protocol));
}

int niohttp_response_nset_protocol(struct niohttp_response *resp,
		const char *protocol, size_t len)
{
	int rc;
	char *str;

	if (!resp || !protocol) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	niohttp_debug("setting protocol: %.*s", len, protocol);

	if (!(str = malloc(len + 1))) {
		niohttp_error("failed to malloc");
		rc = NIOHTTP_ENOMEM;
		goto finally;
	}

	if (resp->protocol) {
		free(resp->protocol);
	}

	resp->protocol = str;
	memcpy(resp->protocol, protocol, len);
	resp->protocol[len] = '\0';
	rc = 0;

finally:
	return rc;
}

inline const char *niohttp_response_version(struct niohttp_response *resp)
{
	return resp->version.string;
}

inline int niohttp_response_major_version(struct niohttp_response *resp)
{
	return resp->version.major;
}

inline int niohttp_response_minor_version(struct niohttp_response *resp)
{
	return resp->version.minor;
}

inline int niohttp_response_set_version(struct niohttp_response *resp,
		const char *version)
{
	return niohttp_response_nset_version(resp, version, strlen(version));
}

inline int niohttp_response_nset_version(struct niohttp_response *resp,
		const char *version, size_t len)
{
	return niohttp_version_nset(&resp->version, version, len);
}

inline int niohttp_response_code(struct niohttp_response *resp)
{
	return resp->code;
}

inline int niohttp_response_set_code(struct niohttp_response *resp, int code)
{
	int rc;

	if (!resp) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	resp->code = code;
	rc = 0;

finally:
	return rc;
}

inline const char *niohttp_response_reason(struct niohttp_response *resp)
{
	return resp->reason;
}

inline int niohttp_response_set_reason(struct niohttp_response *resp,
		const char *reason)
{
	return niohttp_response_nset_reason(resp, reason, strlen(reason));
}

inline int niohttp_response_set_status(struct niohttp_response *resp,
		int code, const char *reason)
{
	int rc;

	if (!resp) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	if ((rc = niohttp_response_set_reason(resp, reason))) {
		niohttp_error("failed to set reason: %s", niohttp_strerror(rc));
		goto finally;
	}

	resp->code = code;
	rc = 0;

finally:
	return rc;
}

int niohttp_response_nset_reason(struct niohttp_response *resp,
		const char *reason, size_t len)
{
	int rc;
	char *str;

	if (!resp || !reason) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	niohttp_debug("setting reason: %.*s", len, reason);

	if (!(str = malloc(len + 1))) {
		niohttp_error("failed to malloc");
		rc = NIOHTTP_ENOMEM;
		goto finally;
	}

	if (resp->reason) {
		free(resp->reason);
	}

	resp->reason = str;
	memcpy(resp->reason, reason, len);
	resp->reason[len] = '\0';
	rc = 0;

finally:
	return rc;
}

inline int niohttp_response_set_header(struct niohttp_response *resp,
		const char *name, const char *value)
{
	if (!resp) {
		return NIOHTTP_EINVAL;
	}

	return niohttp_headers_set(resp->header, name, value);
}

inline const char *niohttp_response_header(struct niohttp_response *resp,
		const char *name)
{
	if (!resp || !name) {
		return NULL;
	}

	return niohttp_headers_get(resp->header, name);
}

inline int niohttp_response_remove_header(struct niohttp_response *resp,
		const char *name)
{
	if (!resp) {
		return NIOHTTP_EINVAL;
	}

	return niohttp_headers_remove(resp->header, name);
}

inline struct niohttp_header_list *niohttp_response_list_headers(
		struct niohttp_response *resp)
{
	return &resp->header->list;
}

inline int niohttp_response_clear_headers(struct niohttp_response *resp)
{
	if (!resp) {
		return NIOHTTP_EINVAL;
	}

	niohttp_headers_clear(resp->header);
	return 0;
}

void niohttp_response_reset(struct niohttp_response *resp)
{
	niohttp_debug("resetting");

	resp->state = NIOHTTP_RESP_LINE;
	resp->protocol = resp->version.string = resp->reason = NULL;
	resp->code = 0;
	niohttp_headers_clear(resp->header);
	resp->mark = 0;
	resp->token = resp->value = NULL;
}

int niohttp_response_parse(struct niohttp_response *resp,
		struct niobuffer *buffer)
{
	int rc, b;
	unsigned char *data = niobuffer_array(buffer);

	niohttp_debug("skipping parsed: %d", resp->mark);

	if ((rc = niobuffer_skip(buffer, resp->mark))) {
		niohttp_error("failed to skip buffer: %s",
				nio_strerror(rc));
		goto finally;
	}

	while ((resp->state != NIOHTTP_RESP_METADATA) &&
			niobuffer_remaining(buffer) > 0) {
		switch (resp->state) {
		case NIOHTTP_RESP_LINE:
			if ((rc = niohttp_response_parse_line(resp, buffer))) {
				niohttp_error("failed to parse first line");
				goto finally;
			}
			break;
		case NIOHTTP_RESP_HEADER_NAME:
			if (!resp->token && data[niobuffer_position(buffer)]
			                         == '\r') {
				niohttp_debug("hit end of headers: %d",
						niobuffer_position(buffer));

				if ((rc = niobuffer_skip(buffer, 1))) {
					niohttp_error("failed to skip: %s",
							nio_strerror(rc));
					goto finally;
				}

				resp->state = NIOHTTP_RESP_HEADER;
				break;
			}
		case NIOHTTP_RESP_HEADER_VALUE:
		case NIOHTTP_RESP_HEADER_ENTRY:
			if ((rc = niohttp_response_parse_header(
					resp, buffer))) {
				niohttp_error("failed to parse header");
				goto finally;
			}
			break;
		case NIOHTTP_RESP_HEADER:
			if ((rc = niobuffer_get(buffer, &b))) {
				niohttp_error("failed to get buffer: %s",
						nio_strerror(rc));
				goto finally;
			}

			if ((b < ' ' || b > '~') && (b != '\r' && b != '\n')) {
				niohttp_error("invalid encoding");
				rc = -1;
				goto finally;
			}

			if (b != '\n') {
				niohttp_error("unexpected end char: %d", b);
				rc = -1;
				goto finally;
			}

			niohttp_debug("hit end of meta data: %d",
					niobuffer_position(buffer) - 1);
			resp->state = NIOHTTP_RESP_METADATA;
			break;
		default:
			niohttp_error("unexpected state: %d", resp->state);
			rc = -1;
			goto finally;
		}
	}

	niohttp_debug("marking parsed: %d", niobuffer_position(buffer));

	resp->mark = niobuffer_position(buffer);
	rc = 0;

finally:
	return rc;
}

int niohttp_response_parse_line(struct niohttp_response *resp,
		struct niobuffer *buffer)
{
	int rc, b;
	unsigned char *data = niobuffer_array(buffer);
	size_t offset;

	niohttp_debug("parsing line");

	while (niobuffer_remaining(buffer) > 0) {
		if (!resp->token) {
			niohttp_debug("marking token at: %d",
					niobuffer_position(buffer));
			resp->token = (char *)data + niobuffer_position(buffer);
		}

		if ((rc = niobuffer_get(buffer, &b))) {
			niohttp_error("failed to get buffer: %s",
					nio_strerror(rc));
			goto finally;
		}

		if ((b < ' ' || b > '~') && (b != '\r' && b != '\n')) {
			niohttp_error("invalid encoding");
			rc = -1;
			goto finally;
		}

		niohttp_trace("processing: %d", b);

		if (!resp->protocol) {
			if (b == '/') {
				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("hit end of protocol: %d",
						offset);

				data[offset] = '\0';
				resp->protocol = resp->token;
				resp->token = NULL;
			}
		} else if (!resp->version.string) {
			if (b == ' ') {
				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("hit end of version: %d", offset);

				data[offset] = '\0';
				resp->version.string = resp->token;
				resp->token = NULL;
			}
		} else if (resp->code == 0) {
			if (b == ' ') {
				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("hit end of code: %d", offset);

				data[offset] = '\0';
				resp->code = strtol(resp->token, NULL, 10);

				if (!resp->code) {
					niohttp_error("invalid numeric format"
							" of code");
					rc = -1;
					goto finally;
				}

				resp->token = NULL;
			}
		} else if (!resp->reason) {
			if (b == '\r') {
				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("hit end of reason: %d", offset);

				data[offset] = '\0';
				resp->reason = resp->token;
				resp->token = NULL;
			}
		} else {
			if (b != '\n') {
				niohttp_error("unexpected end char: %d", b);
				rc = -1;
				goto finally;
			}

			niohttp_debug("hit end of first line");

			resp->token = NULL;
			resp->state = NIOHTTP_RESP_HEADER_NAME;
			break;
		}

		niohttp_trace("continuing");
	}

	rc = 0;

finally:
	return rc;
}

int niohttp_response_parse_header(struct niohttp_response *resp,
		struct niobuffer *buffer)
{
	int rc, b;
	unsigned char *data = niobuffer_array(buffer);
	size_t offset;

	niohttp_debug("parsing header entry");

	while (niobuffer_remaining(buffer) > 0) {
		if (!resp->token) {
			niohttp_debug("marking header name at: %d",
					niobuffer_position(buffer));

			resp->token = (char *)data + niobuffer_position(buffer);
		}

		if ((rc = niobuffer_get(buffer, &b))) {
			niohttp_error("failed to get buffer: %s",
					nio_strerror(rc));
			goto finally;
		}

		if ((b < ' ' || b > '~') && (b != '\r' && b != '\n')) {
			niohttp_error("invalid encoding");
			rc = -1;
			goto finally;
		}

		niohttp_trace("processing: %d", b);

		if (resp->state == NIOHTTP_RESP_HEADER_NAME) {
			if (b == ':') {
				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("hit end of header name: %d",
						offset);

				data[offset] = '\0';
				resp->state = NIOHTTP_RESP_HEADER_VALUE;
				resp->value = NULL;
			}
		} else if (resp->state == NIOHTTP_RESP_HEADER_VALUE) {
			if (!resp->value) {
				if (b == ' ') {
					continue;
				}

				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("marking header value at: %d",
						offset);
				resp->value = (char *)data + offset;
			}

			if (b == '\r') {
				offset = niobuffer_position(buffer) - 1;

				niohttp_debug("hit end of header value: %d",
						offset);

				data[offset] = '\0';

				if ((rc = niohttp_headers_set(resp->header,
						resp->token, resp->value))) {
					niohttp_error("failed to set header:"
							" %s => %s",
							resp->token,
							resp->value);
					goto finally;
				}

				resp->state = NIOHTTP_RESP_HEADER_ENTRY;
			}
		} else if (resp->state == NIOHTTP_RESP_HEADER_ENTRY) {
			if (b != '\n') {
				niohttp_error("unexpected end char: %d", b);
				rc = -1;
				goto finally;
			}

			offset = niobuffer_position(buffer) - 1;

			niohttp_debug("hit end of header entry: %d", offset);

			resp->state = NIOHTTP_RESP_HEADER_NAME;
			resp->token = NULL;
			break;
		}

		niohttp_trace("continuing");
	}

	rc = 0;

finally:
	return rc;
}
